什麼是閉包 (Closure)?


Posted by Kai on 2020-12-27

在 func1 中 return 另一個 func2

好處:在 func1 中的變數 可被 func2 使用,而 func1 的變數不會被外層讀取。
壞處:資料不會正常的被銷毀,而會一直留著。

執行的內部函式的方式會是 fun1()() 後方括弧表示呼叫內部函式,假如將函式賦予在另一個變數上,內部的變數就會存在內層,而隨著函式執行而不斷改變。
如同以下的範例:

function test() {
    let a = 10
    return function c(b) {
        a = a - b
        return a
    }
}
let d = test()
d(1) // 9
d(1) // 8
d(1) // 7

閉包的原理


那閉包的原理又是什麼呢?為什麼當中的變數會留著而記憶體不會被釋放呢?這時候就需要提到另一個東西了,叫做範圍鏈 (Scope Chain)

範圍鏈 (Scope Chain)

Execution Contexts (執行環境) 被建立時, Scope Chain、變數、 this 都被建立且初始化。

進入 function EC 的時候,範圍鏈的初始化會包含一個 Activation Object (AO) 以及 [[Scope]],function 的 [[Scope]] 是當被宣告時而決定的,也就是 scope chain 會是 [AO, [[Scope]] ]

Activation Object (AO)

當 function 被建立時,AO 被初始化,且會有一個 arguments 的屬性,而 AO 可以當作 VO 來使用,而 VO 就是前面所說的,在每個執行環境建立時都會有一個 Variable Object (VO),用於存放被宣告的變數及函式等。

    var a = 1
    function test() {
        var b = 2
        function test2() {
            var c = 3
            console.log(b)
            console.log(a)
        }
        test2()
    }
    test()

用以上的程式碼作為示範,整體的執行堆會像以下所寫的部分:

    test2 EC: {
        AO: {
            c:3
        },
        scopeChain: [test2EC.AO, test2.[[Scope]]]
        // [test2EC.AO, test2EC.[[Scope]]] = [test2EC.AO, testEC.scopeChain] = [test2EC.AO, testEC.AO, globalEC.VO]
    }
    test EC: {
        AO: {
            b: 2,
            test2: function
        },
        scopeChain: [testEC.AO, test.[[Scope]]]
        // [testEC.AO, testEC.[[Scope]]] = [testEC.AO, globalEC.VO] 
    }
    globalEC: {
        VO: {
            a: 1,
            test: function
        },
        scopeChain: [globalEC.VO]
    }

首先會初始化的是 Global EC

裡面會有一個 VO 並宣告變數 a = undefinedtest = func

再來建立自己的 Scope Chain 也就是 GlobalEC.VO,最後賦值 a = 1

呼叫 test() 於是,test EC 被建立,並疊在 Global EC 上,形成執行堆,

由於是函式,因此建立的會是 AO 並宣告變數 b = undefinedtest2 = func

建立 Scope Chain,函式的 Scope Chain 會是

testEC.AO, testEC.[[Scope]]

testEC.[[Scope]] = GlobalEC.scopeChain = GlobalEC.VO

也就是說,testEC.scopeChain = [testEC.AO, GlobalEC.VO]

之後賦值 b = 2

呼叫 test2() 依照同樣的步驟,test2EC.scopeChain

會是 test2EC.AO, [[Scope]]

等同於 test2EC.AO, testEC.scopeChain

再拆成 test2EC.AO, testEC.AO, globlaEC.scopeChain

最後變成 test2EC.AO, testEC.AO, globlaEC.VO

這樣就可以很清楚的看出來,test2() 執行後,log 出的資料會是 2 跟 1,用 letconst 也會是同樣的結果,因為範圍鏈的關係,所以內層的函式可以藉由外層的 VO/AO 而得到變數。

示範閉包


function test() {
    let a = 10
    return function c(b) {
        a = a - b
        return a
    }
}
let d = test()
d(1)

用一開始的函式做示範,他的執行堆就會如同以下的部分:

cEC:{
    AO: {
        b: undefined
    }
    scopeChain: [cEC.AO, testEC.AO, globalEC.VO]
}

testEC: {
    AO: {
        a: 10,
        c: function
    }
    scopeChain: [testEC.AO, globalEC.VO]
}
globalEC: {
    VO: {
        d: function,
        test: function
    }
    scopeChain: [globalEC.VO]
}

正常情況下,testEC.AO 在沒有被使用的情況下,記憶體會被釋放,但由於 function c 被 return,而且其範圍鏈中有 testEC.AO ,因此記憶體沒有被釋放,所以 a 這個變數才被保留下來,而這個就是閉包的原理。

有趣的是,以上是我們在文章上常看見對於閉包的解釋,但實際上在 JavaScropt 中,所有函式都可以被認為是閉包,主要的原因可以參考 Huli 大大所寫的 所有的函式都是閉包:談 JS 中的作用域與 Closure

以上筆記參考 Huli 的課程 [JS201] 進階 JavaScript:那些你一直搞不懂的地方


#closure #javascript #ScopeChain







Related Posts

超讚 Deep Learning on 3D object detection 相關教學影片彙整

超讚 Deep Learning on 3D object detection 相關教學影片彙整

【THM Walkthrough】Lateral Movement and Pivoting (1)

【THM Walkthrough】Lateral Movement and Pivoting (1)

【JS上課筆記】JavaScript 物件導向:建構式、class、原型鏈

【JS上課筆記】JavaScript 物件導向:建構式、class、原型鏈


Comments